返回 引气・Java 气海初拓

15重走我的Java Day15:当我的代码突然有了“读心术”:反射和注解的魔法之旅

博主
大约 15 分钟

15重走我的Java Day15:当我的代码突然有了“读心术”:反射和注解的魔法之旅

第一次看到同事用@Autowired时,我以为这是某种魔法。直到我自己写测试时,面对一个300行的方法无从下手,才明白:好的代码不仅要能运行,还要能被测试、被扩展、被理解

一、单元测试:从“自信满满”到“小心翼翼”

image-20260201230337149

1.1 我以为我的代码坚如磐石

“测试?我代码写完自己跑一遍不就得了!”——这是我刚开始学习编程时的真实想法。

直到那个周五晚上11点,我提交了一段“完美”的订单处理代码:

java

public class OrderService {
    // 我自豪的“高效”方法 - 整合了所有业务逻辑
    public OrderResult processOrder(Order order, User user, Payment payment) {
        // 验证订单(100行)
        if (!validateOrder(order)) {
            throw new ValidationException("订单无效");
        }
        
        // 检查库存(80行)
        if (!checkInventory(order.getItems())) {
            throw new InventoryException("库存不足");
        }
        
        // 处理支付(120行)
        PaymentResult paymentResult = processPayment(payment, order.getTotal());
        if (!paymentResult.isSuccess()) {
            throw new PaymentException("支付失败");
        }
        
        // 更新库存、生成物流、发送通知...(200行)
        // ...
        
        return new OrderResult(true, "订单处理成功");
    }
}

周一早上,运维的电话把我从梦中惊醒:“你的代码把测试环境的数据库锁死了!”

原来,我的“高效”方法:

  1. 无法单独测试:想测支付逻辑?必须构造完整的订单
  2. 错误难以定位:出问题时不知道是哪部分逻辑有问题
  3. 运行缓慢:每次测试都要走完整流程

1.2 JUnit教会我的“测试思维”

导师给我看了他的测试代码:

java

// 测试一个简单的计算器
class CalculatorTest {
    private Calculator calculator;
    
    @BeforeEach  // 每个测试前都会执行
    void setUp() {
        calculator = new Calculator();
        System.out.println("准备测试环境...");
    }
    
    @Test
    @DisplayName("测试加法:2 + 3 = 5")
    void testAdd() {
        // 准备测试数据
        int a = 2;
        int b = 3;
        
        // 执行被测方法
        int result = calculator.add(a, b);
        
        // 验证结果
        assertEquals(5, result, "2 + 3 应该等于 5");
        
        // 额外的断言
        assertTrue(result > 0, "结果应该是正数");
    }
    
    @Test
    @DisplayName("除以零应该抛出异常")
    void testDivideByZero() {
        // 断言会抛出特定异常
        assertThrows(ArithmeticException.class, () -> {
            calculator.divide(10, 0);
        });
    }
    
    @Test
    @DisplayName("批量测试加法")
    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5", 
        "10, 20, 30"
    })
    void testAddMultiple(int a, int b, int expected) {
        assertEquals(expected, calculator.add(a, b));
    }
    
    @AfterEach  // 每个测试后都会执行
    void tearDown() {
        System.out.println("清理测试环境...");
        calculator = null;
    }
}

我学到的第一课:写测试不是可有可无的步骤,而是设计的一部分

1.3 重构后的可测试代码

我把那个300行的怪物拆分了:

java

// 每个职责独立,可以单独测试
public class OrderService {
    private OrderValidator validator;
    private InventoryService inventoryService;
    private PaymentService paymentService;
    
    // 现在每个方法都可以单独测试
    public OrderResult processOrder(Order order) {
        validator.validate(order);
        inventoryService.reserve(order.getItems());
        PaymentResult payment = paymentService.charge(order);
        // ...
    }
}

// 现在我可以这样测试
@Test
void testOrderValidation() {
    Order invalidOrder = createInvalidOrder();
    assertThrows(ValidationException.class, () -> {
        orderService.processOrder(invalidOrder);
    });
}

@Test 
void testPaymentFailure() {
    Order order = createValidOrder();
    mockPaymentServiceToFail();  // 模拟支付失败
    
    OrderResult result = orderService.processOrder(order);
    
    assertFalse(result.isSuccess());
    assertEquals("支付失败", result.getMessage());
}

二、反射:第一次发现Java会“读心术”

image-20260201230352055

2.1 那个让我熬夜的配置文件解析器

我需要写一个通用配置加载器,把properties文件映射到Java对象:

java

// 用户配置文件
# application.properties
app.name=我的应用
app.version=1.0.0
server.port=8080
database.url=jdbc:mysql://localhost:3306/test

// 对应的配置类
public class AppConfig {
    private String appName;
    private String appVersion;
    private int serverPort;
    private String databaseUrl;
    
    // 一大堆getter和setter...
}

我的第一个版本写了200行硬编码:

java

public AppConfig loadConfig(String filename) {
    AppConfig config = new AppConfig();
    Properties props = loadProperties(filename);
    
    // 硬编码每个字段的映射
    config.setAppName(props.getProperty("app.name"));
    config.setAppVersion(props.getProperty("app.version"));
    config.setServerPort(Integer.parseInt(props.getProperty("server.port")));
    // ... 每加一个字段就要改这里
    
    return config;
}

每次新增配置字段,我都要修改两处:配置类和加载逻辑。

2.2 反射的“顿悟”时刻

直到我发现了反射:

java

public <T> T loadConfig(Class<T> configClass, String filename) {
    try {
        T config = configClass.newInstance();  // 创建对象
        Properties props = loadProperties(filename);
        
        // 获取所有字段
        Field[] fields = configClass.getDeclaredFields();
        
        for (Field field : fields) {
            field.setAccessible(true);  // 允许访问私有字段
            
            // 根据字段名找配置(约定:驼峰转点分隔)
            String propName = camelToDot(field.getName());
            String value = props.getProperty(propName);
            
            if (value != null) {
                // 根据字段类型转换值
                Object convertedValue = convertValue(value, field.getType());
                field.set(config, convertedValue);  // 设置字段值
            }
        }
        
        return config;
        
    } catch (Exception e) {
        throw new RuntimeException("加载配置失败", e);
    }
}

魔法发生了:现在我只需要定义配置类,加载器会自动处理映射。新增字段?只需在类中添加,一行加载代码都不用改!

2.3 反射的实际应用:一个简单的ORM

理解了反射后,我甚至写了个迷你ORM:

java

// 注解:标记数据库表
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
    String name();  // 表名
}

// 注解:标记字段对应列
public @interface Column {
    String name();
    boolean nullable() default true;
}

// 实体类
@Table(name = "users")
public class User {
    @Column(name = "id", nullable = false)
    private Long id;
    
    @Column(name = "username")
    private String username;
    
    @Column(name = "email")
    private String email;
    
    // getter/setter省略
}

// 使用反射生成SQL
public class SimpleORM {
    public String buildInsertSQL(Object entity) {
        Class<?> clazz = entity.getClass();
        
        // 获取表名
        Table table = clazz.getAnnotation(Table.class);
        String tableName = table.name();
        
        // 获取所有字段
        StringBuilder columns = new StringBuilder();
        StringBuilder values = new StringBuilder();
        
        for (Field field : clazz.getDeclaredFields()) {
            Column column = field.getAnnotation(Column.class);
            if (column != null) {
                field.setAccessible(true);
                
                try {
                    Object value = field.get(entity);
                    if (value != null) {
                        columns.append(column.name()).append(", ");
                        values.append("'").append(value).append("', ");
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
        
        // 去掉最后的逗号和空格
        if (columns.length() > 0) {
            columns.setLength(columns.length() - 2);
            values.setLength(values.length() - 2);
        }
        
        return String.format("INSERT INTO %s (%s) VALUES (%s)", 
                           tableName, columns, values);
    }
}

// 使用
User user = new User();
user.setId(1L);
user.setUsername("张三");
user.setEmail("zhangsan@example.com");

String sql = orm.buildInsertSQL(user);
// 输出:INSERT INTO users (id, username, email) VALUES ('1', '张三', 'zhangsan@example.com')

三、动态代理:给代码加上“监控摄像头”

image-20260201230408779

3.1 那个重复的日志代码

我曾经在每个业务方法里都写这样的代码:

java

public class UserService {
    public User getUserById(Long id) {
        long start = System.currentTimeMillis();
        System.out.println("开始查询用户,ID: " + id);
        
        try {
            // 业务逻辑
            User user = userDao.findById(id);
            System.out.println("查询成功,耗时: " + (System.currentTimeMillis() - start) + "ms");
            return user;
        } catch (Exception e) {
            System.out.println("查询失败: " + e.getMessage());
            throw e;
        }
    }
    
    public void updateUser(User user) {
        long start = System.currentTimeMillis();
        System.out.println("开始更新用户,ID: " + user.getId());
        
        try {
            // 业务逻辑
            userDao.update(user);
            System.out.println("更新成功,耗时: " + (System.currentTimeMillis() - start) + "ms");
        } catch (Exception e) {
            System.out.println("更新失败: " + e.getMessage());
            throw e;
        }
    }
    // 每个方法都要重复这个模式...
}

代码重复,难以维护,业务逻辑和横切关注点(日志、性能监控)混在一起。

3.2 JDK动态代理:一次编写,到处记录

java

// 1. 定义接口
public interface UserService {
    User getUserById(Long id);
    void updateUser(User user);
    void deleteUser(Long id);
}

// 2. 实现类(只关注业务逻辑)
public class UserServiceImpl implements UserService {
    public User getUserById(Long id) {
        // 纯业务逻辑,没有日志代码
        return userDao.findById(id);
    }
    
    public void updateUser(User user) {
        userDao.update(user);
    }
    
    public void deleteUser(Long id) {
        userDao.delete(id);
    }
}

// 3. 代理处理器(统一处理日志)
public class LoggingHandler implements InvocationHandler {
    private Object target;  // 被代理的对象
    
    public LoggingHandler(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        long start = System.currentTimeMillis();
        
        System.out.println("【开始执行】" + methodName + ",参数: " + Arrays.toString(args));
        
        try {
            // 调用原始方法
            Object result = method.invoke(target, args);
            
            long duration = System.currentTimeMillis() - start;
            System.out.println("【执行成功】" + methodName + ",耗时: " + duration + "ms");
            
            return result;
        } catch (Exception e) {
            System.out.println("【执行失败】" + methodName + ",错误: " + e.getMessage());
            throw e;
        }
    }
}

// 4. 创建代理对象
public class ServiceFactory {
    public static UserService createUserService() {
        UserService realService = new UserServiceImpl();
        
        // 创建代理
        UserService proxy = (UserService) Proxy.newProxyInstance(
            realService.getClass().getClassLoader(),  // 类加载器
            realService.getClass().getInterfaces(),   // 实现的接口
            new LoggingHandler(realService)           // 代理处理器
        );
        
        return proxy;
    }
}

// 5. 使用代理
public class Main {
    public static void main(String[] args) {
        // 获取的是代理对象,不是真实对象
        UserService userService = ServiceFactory.createUserService();
        
        // 调用方法时,会自动记录日志
        User user = userService.getUserById(1L);
        userService.updateUser(user);
    }
}

神奇的效果:现在我的业务代码很干净,所有方法自动拥有了日志、性能监控、事务管理等功能。

3.3 动态代理的更多应用场景

java

// 权限检查代理
public class SecurityHandler implements InvocationHandler {
    private Object target;
    private User currentUser;
    
    public SecurityHandler(Object target, User currentUser) {
        this.target = target;
        this.currentUser = currentUser;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 检查注解
        RequiresPermission permission = method.getAnnotation(RequiresPermission.class);
        if (permission != null) {
            String requiredRole = permission.value();
            if (!currentUser.hasRole(requiredRole)) {
                throw new SecurityException("权限不足,需要角色: " + requiredRole);
            }
        }
        
        return method.invoke(target, args);
    }
}

// 缓存代理
public class CacheHandler implements InvocationHandler {
    private Object target;
    private Map<String, Object> cache = new HashMap<>();
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 生成缓存键:方法名+参数
        String cacheKey = generateCacheKey(method, args);
        
        // 检查缓存
        if (cache.containsKey(cacheKey)) {
            System.out.println("【缓存命中】" + cacheKey);
            return cache.get(cacheKey);
        }
        
        // 调用真实方法
        Object result = method.invoke(target, args);
        
        // 放入缓存
        cache.put(cacheKey, result);
        System.out.println("【缓存设置】" + cacheKey);
        
        return result;
    }
}

四、组合使用:一个迷你Spring框架

image-20260201230423240

当我把这些技术组合起来时,我仿佛看到了Spring框架的雏形:

java

// 1. 定义一些“Spring风格”的注解
@Component
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
    String value() default "";
}

@Autowired
@Target({ElementType.FIELD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}

// 2. 容器类(迷你IoC容器)
public class MiniContainer {
    private Map<String, Object> beans = new HashMap<>();
    
    public void register(Class<?>... componentClasses) {
        for (Class<?> clazz : componentClasses) {
            if (clazz.isAnnotationPresent(Component.class)) {
                try {
                    // 创建实例
                    Object instance = clazz.newInstance();
                    
                    // 获取组件名
                    Component component = clazz.getAnnotation(Component.class);
                    String beanName = component.value();
                    if (beanName.isEmpty()) {
                        beanName = clazz.getSimpleName();
                    }
                    
                    // 存入容器
                    beans.put(beanName, instance);
                    
                    System.out.println("注册Bean: " + beanName + " -> " + clazz.getName());
                } catch (Exception e) {
                    throw new RuntimeException("创建组件失败: " + clazz.getName(), e);
                }
            }
        }
        
        // 依赖注入
        injectDependencies();
    }
    
    private void injectDependencies() {
        for (Object bean : beans.values()) {
            Class<?> clazz = bean.getClass();
            
            // 注入字段
            for (Field field : clazz.getDeclaredFields()) {
                if (field.isAnnotationPresent(Autowired.class)) {
                    field.setAccessible(true);
                    
                    // 按类型查找依赖
                    Object dependency = findBeanByType(field.getType());
                    if (dependency != null) {
                        try {
                            field.set(bean, dependency);
                            System.out.println("注入依赖: " + clazz.getSimpleName() + 
                                             "." + field.getName() + " <- " + 
                                             dependency.getClass().getSimpleName());
                        } catch (IllegalAccessException e) {
                            throw new RuntimeException("注入依赖失败", e);
                        }
                    }
                }
            }
        }
    }
    
    @SuppressWarnings("unchecked")
    public <T> T getBean(Class<T> requiredType) {
        return (T) findBeanByType(requiredType);
    }
    
    private Object findBeanByType(Class<?> requiredType) {
        for (Object bean : beans.values()) {
            if (requiredType.isAssignableFrom(bean.getClass())) {
                return bean;
            }
        }
        return null;
    }
}

// 3. 定义一些组件
@Component("userService")
public class UserService {
    @Autowired
    private UserRepository userRepository;  // 会自动注入
    
    public User findUser(Long id) {
        return userRepository.findById(id);
    }
}

@Component("userRepository") 
public class UserRepository {
    public User findById(Long id) {
        // 模拟数据库查询
        return new User(id, "张三");
    }
}

// 4. 使用迷你容器
public class Application {
    public static void main(String[] args) {
        MiniContainer container = new MiniContainer();
        
        // 注册组件(自动创建实例和注入依赖)
        container.register(UserService.class, UserRepository.class);
        
        // 获取Bean
        UserService userService = container.getBean(UserService.class);
        
        // 使用
        User user = userService.findUser(1L);
        System.out.println("找到用户: " + user.getName());
    }
}

运行这个程序,你会看到:

text

注册Bean: userService -> UserService
注册Bean: userRepository -> UserRepository
注入依赖: UserService.userRepository <- UserRepository
找到用户: 张三

五、经验总结:从“能用”到“好用”的转变

5.1 我学到的编程哲学

  1. 测试驱动设计:如果一个方法难以测试,通常意味着设计有问题
  2. 关注点分离:业务逻辑、日志、安全、事务应该分开处理
  3. 约定优于配置:通过注解和反射,减少重复的配置代码
  4. 动态扩展:通过代理模式,在不修改源码的情况下增强功能

5.2 实际项目中的使用建议

单元测试

  • 先写测试,再写实现(TDD)
  • 每个测试只测一个功能点
  • 使用Mock对象隔离依赖

反射

  • 框架开发中使用,业务代码中慎用
  • 注意性能影响,适当缓存反射结果
  • 处理好各种异常情况

注解

  • 定义清晰的命名和默认值
  • 提供详细的文档说明
  • 考虑向后兼容性

动态代理

  • 接口代理用JDK动态代理
  • 类代理用CGLIB
  • Spring AOP已经封装得很好,直接使用即可

5.3 一个简单的检查清单

当你写代码时,问自己这些问题:

  • 我的代码容易写单元测试吗?
  • 有没有重复的横切关注点(日志、事务)可以抽取?
  • 是否可以通过注解减少配置?
  • 是否需要在不修改源码的情况下扩展功能?

结语:从“写代码”到“设计代码”

image-20260201230441654

学习这些高级特性之前,我只是在“写代码”——让程序运行起来就行。

学习之后,我开始“设计代码”——思考如何让代码更可测试、更可维护、更可扩展。

最大的收获不是学会了反射或代理的技术细节,而是学会了思考代码的“元”问题:这段代码未来会怎样变化?别人如何理解和使用它?如何让它适应未知的需求?

现在,当我看到@Autowired时,我不再觉得这是魔法。我知道这背后是反射、是动态代理、是设计模式的巧妙组合。更重要的是,我知道何时使用这些技术,何时保持简单。

记住:最酷的技术不是最复杂的技术,而是最合适的技术。 有时候,一个简单的设计比过度工程化的“高级”方案更有价值。

知识点测试

读完文章了?来测试一下你对知识点的掌握程度吧!

评论区

使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。

如果评论系统无法加载,请确保:

  • 您的网络可以访问 GitHub
  • giscus GitHub App 已安装到仓库
  • 仓库已启用 Discussions 功能